﻿using System;
using System.Linq.Expressions;

namespace TelerikMvcGridCustomBindingHelper.Util
{
    internal interface IContract<T>
    {
        IContract<T> Is<TT>();
        IContract<T> IsEqual<TT>(TT of) where TT : T, IEquatable<TT>;
        IContract<T> IsNotEqual<TT>(TT of) where TT : T, IEquatable<TT>;
        IContract<T> IsGreatherThan<TT>(TT of) where TT : T, IComparable;
        IContract<T> IsGreatherThanOrEqual<TT>(TT of) where TT : T, IComparable;
        IContract<T> IsLessThan<TT>(TT of) where TT : T, IComparable;
        IContract<T> IsLessThanOrEqual<TT>(TT of) where TT : T, IComparable;
        IContract<T> IsNot<TT>();
        IContract<T> IsNull();
        IContract<T> IsNotNull();
        IContract<T> IsTheSameOf(object of);
        IContract<T> IsNotTheSameOf(object of);
        IContract<T> Meet(Expression<Predicate<T>> predicate);
        IContractThrow<TOther> And<TOther>(Expression<Func<TOther>> exp);

        IAssignContract<T> And();
    }

    internal interface IAssignContract<T>
    {
        IContract<T> Expect(Expression<Func<T>> exp);
    }

    internal interface IContractThrow<T> : IContract<T>
    {
        IContractWhen<T> Throw(Func<string, Exception> thrower);
        IContractWhen<T> Throw(Exception thrower);
    }

    internal interface IContractWhen<T>
    {
        IContract<T> WhenViolate { get; }
    }

    internal class Contract
    {
        private Contract() { }

        class ContractOn<T> : Contract, IAssignContract<T>, IContractThrow<T>, IContractWhen<T>
        {
            readonly Func<T> _accessor;
            private string Name { get; set; }

            public ContractOn(Func<T> accessor, string name)
            {
                _accessor = accessor;
                Name = name;
            }

            public IContract<T> IsNotNull()
            {
                if (Equals(_accessor(), null))
                    InternalThrow(() => { throw new ArgumentNullException(Name); });
                return this;
            }

            public IContract<T> Is<TT>()
            {
                if ((_accessor() is TT) == false)
                    InternalThrow(() => { throw new ArgumentException("Type not expected, must be: " + typeof(TT).Name, Name); });
                return this;
            }

            public IContract<T> Meet(Expression<Predicate<T>> predicate)
            {
                if (false == (bool)predicate.Compile().DynamicInvoke(_accessor()))
                    InternalThrow(() => { throw new ArgumentException("Precondition not verified: " + Rename(predicate), Name); });
                return this;
            }

            public IAssignContract<T> And()
            {
                return this;
            }

            public IContractThrow<TOther> And<TOther>(Expression<Func<TOther>> exp)
            {
                return Expect(exp);
            }

            private string Rename(Expression<Predicate<T>> predicate)
            {
                var l = predicate as LambdaExpression;
                if (l != null && l.Parameters.Count > 0)
                {
                    var pnew = PredicateRewriter.Rewrite(predicate, Name);
                    return pnew.Body.ToString();
                }

                return string.Empty;
            }

            public IContract<T> IsNot<TT>()
            {
                if (_accessor() is TT)
                    InternalThrow(() => { throw new ArgumentException("Type not expected, must not be: " + typeof(TT).Name, Name); });
                return this;
            }

            public IContract<T> IsTheSameOf(object of)
            {
                if (ReferenceEquals(_accessor(), of) == false)
                    InternalThrow(() => { throw new ArgumentException("Not the same as expected", Name); });
                return this;
            }

            public IContract<T> IsNotTheSameOf(object of)
            {
                if (ReferenceEquals(_accessor(), of))
                    InternalThrow(() => { throw new ArgumentException("Is expected to be different", Name); });
                return this;
            }

            public IContract<T> IsNull()
            {
                if (Equals(_accessor(), null) == false)
                    InternalThrow(() => { throw new ArgumentException("Must be null", Name); });
                return this;
            }

            public IContract<T> IsGreatherThan<TT>(TT of) where TT : T, IComparable
            {
                var actual = (TT)_accessor();
                if (actual.CompareTo(of) <= 0)
                    InternalThrow(() => { throw new ArgumentOutOfRangeException(Name, string.Format("Must be greather than {0}", of)); });
                return this;
            }

            public IContract<T> IsEqual<TT>(TT of) where TT : T, IEquatable<TT>
            {
                var actual = (TT)_accessor();
                if (!actual.Equals(of))
                    InternalThrow(() => { throw new ArgumentException(string.Format("Must be equal to {0}", of), Name); });
                return this;
            }

            public IContract<T> IsNotEqual<TT>(TT of) where TT : T, IEquatable<TT>
            {
                var actual = (TT)_accessor();
                if (actual.Equals(of))
                    InternalThrow(() => { throw new ArgumentException(string.Format("Must not be equal to {0}", of), Name); });
                return this;
            }

            public IContract<T> IsGreatherThanOrEqual<TT>(TT of) where TT : T, IComparable
            {
                var actual = (TT)_accessor();
                if (actual.CompareTo(of) < 0)
                    InternalThrow(() => { throw new ArgumentOutOfRangeException(Name, string.Format("Must be greather than or equal {0}", of)); });
                return this;
            }

            public IContract<T> IsLessThan<TT>(TT of) where TT : T, IComparable
            {
                var actual = (TT)_accessor();
                if (actual.CompareTo(of) >= 0)
                    InternalThrow(() => { throw new ArgumentOutOfRangeException(Name, string.Format("Must be less than {0}", of)); });
                return this;
            }

            public IContract<T> IsLessThanOrEqual<TT>(TT of) where TT : T, IComparable
            {
                var actual = (TT)_accessor();
                if (actual.CompareTo(of) > 0)
                    InternalThrow(() => { throw new ArgumentOutOfRangeException(Name, "Must be less than or equal" + of); });
                return this;
            }

            ExceptionToThrow _exceptionToThrow;

            private class ExceptionToThrow
            {
                private readonly Exception _exception;
                private readonly Func<string, Exception> _thrower;

                public ExceptionToThrow(Func<string, Exception> thrower)
                {
                    _thrower = thrower;
                }

                public ExceptionToThrow(Exception exception)
                {
                    _exception = exception;
                }

                public Exception Throw(string arg)
                {
                    return _thrower != null ? _thrower(arg) : _exception;
                }
            }

            void InternalThrow(Action @default)
            {
                if (_exceptionToThrow == null)
                    @default();
                else
                    throw (_exceptionToThrow.Throw(Name));
            }

            public IContractWhen<T> Throw(Func<string, Exception> thrower)
            {
                if (_exceptionToThrow == null)
                    _exceptionToThrow = new ExceptionToThrow(thrower);
                else
                    throw new Exception("Throw should be specified at most once");

                return this;
            }

            public IContractWhen<T> Throw(Exception exception)
            {
                if (_exceptionToThrow == null)
                    _exceptionToThrow = new ExceptionToThrow(exception);
                else
                    throw new Exception("Throw should be specified at most once");

                return this;
            }

            public IContract<T> Expect(Expression<Func<T>> exp)
            {
                if (exp.Body.NodeType == ExpressionType.MemberAccess)
                {
                    var memberExpression = exp.Body as MemberExpression;
                    if (memberExpression != null)
                    {
                        var name = memberExpression.Member.Name;
                        return new ContractOn<T>(exp.Compile(), name);
                    }
                }

                if (exp.Body.NodeType == ExpressionType.Constant)
                    return new ContractOn<T>(exp.Compile(), "constant");

                if (exp.Body.NodeType == ExpressionType.TypeAs)
                    return new ContractOn<T>(exp.Compile(), exp.ToString());

                throw new Exception("Unexpected expression");
            }

            #region IContractWhen<T> Members

            public IContract<T> WhenViolate
            {
                get { return this; }
            }

            #endregion
        }

        public static IContractThrow<T> Expect<T>(Expression<Func<T>> exp)
        {
            return (IContractThrow<T>)new ContractOn<T>(exp.Compile(), string.Empty).Expect(exp);
        }

        static class PredicateRewriter
        {
            public static Expression<Predicate<T>> Rewrite<T>(Expression<Predicate<T>> exp, string newParamName)
            {
                var param = Expression.Parameter(exp.Parameters[0].Type, newParamName);
                var newExpression = new PredicateRewriterVisitor(param).Visit(exp);

                return (Expression<Predicate<T>>)newExpression;
            }

            private class PredicateRewriterVisitor : ExpressionVisitor
            {
                private readonly ParameterExpression _parameterExpression;

                public PredicateRewriterVisitor(ParameterExpression parameterExpression)
                {
                    _parameterExpression = parameterExpression;
                }

                protected override Expression VisitParameter(ParameterExpression node)
                {
                    return _parameterExpression;
                }
            }
        }
    }
}
